using System.Collections.Immutable;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using TUnit.Core.SourceGenerator.CodeGenerators;
using TUnit.Core.SourceGenerator.Generators;
using TUnit.Core.SourceGenerator.Tests.Extensions;
using TUnit.Core.SourceGenerator.Tests.Options;

namespace TUnit.Core.SourceGenerator.Tests;

public class TestsBase
{
    protected TestsBase()
    {
    }

    public TestsBase<TestMetadataGenerator> TestMetadataGenerator = new();
    public TestsBase<AotConverterGenerator> AotConverterGenerator = new();
    public TestsBase<HookMetadataGenerator> HooksGenerator = new();
    public TestsBase<AssemblyLoaderGenerator> AssemblyLoaderGenerator = new();
    public TestsBase<DisableReflectionScannerGenerator> DisableReflectionScannerGenerator = new();
    public TestsBase<DynamicTestsGenerator> DynamicTestsGenerator = new();

    public Task RunTest(string inputFile, Func<string[], Task> assertions)
    {
        return TestMetadataGenerator.RunTest(inputFile, new RunTestOptions(), assertions);
    }

    public Task RunTest(string inputFile, RunTestOptions runTestOptions, Func<string[], Task> assertions)
    {
        return TestMetadataGenerator.RunTest(inputFile, runTestOptions, assertions);
    }
}

public class TestsBase<TGenerator> where TGenerator : IIncrementalGenerator, new()
{
    public Task RunTest(string inputFile, Func<string[], Task> assertions)
    {
        return RunTest(inputFile, new RunTestOptions(), assertions);
    }

    public async Task RunTest(string inputFile, RunTestOptions runTestOptions, Func<string[], Task> assertions)
    {
#if NET
        var source = await File.ReadAllTextAsync(inputFile);
#else
        var source = File.ReadAllText(inputFile);
#endif

        var customAttributes = Sourcy.Git.RootDirectory
            .GetDirectory("TUnit.TestProject")
            .GetDirectory("Attributes")
            .GetFiles("*.cs")
            .Select(x => x.FullName)
            .ToArray();

        runTestOptions.AdditionalFiles =
        [
            ..runTestOptions.AdditionalFiles,
            ..customAttributes
        ];

        string[] additionalSources =
        [
            """
            // <auto-generated/>
            global using global::System;
            global using global::System.Collections.Generic;
            global using global::System.IO;
            global using global::System.Linq;
            global using global::System.Net.Http;
            global using global::System.Threading;
            global using global::System.Threading.Tasks;
            global using global::TUnit.Core;
            global using static global::TUnit.Core.HookType;
            """,
            """
            namespace System.Diagnostics.CodeAnalysis;

            public class ExcludeFromCodeCoverageAttribute : Attribute;
            """,
            """
            namespace System.Diagnostics.CodeAnalysis;

            public class UnconditionalSuppressMessageAttribute : Attribute;
            """,
#if NET
            ..await Task.WhenAll(runTestOptions.AdditionalFiles.Select(x => File.ReadAllTextAsync(x))),
#else
            ..runTestOptions.AdditionalFiles.Select(x => File.ReadAllText(x)),
#endif
            ..runTestOptions.AdditionalSyntaxes,
        ];

        // Create an instance of the source generator.
        var generator = new TGenerator();

        // Source generators should be tested using 'GeneratorDriver'.
        GeneratorDriver driver = CSharpGeneratorDriver.Create(generator);

        if (runTestOptions.BuildProperties != null)
        {
            driver = driver.WithUpdatedAnalyzerConfigOptions(new TestAnalyzerConfigOptionsProvider(
                    runTestOptions.BuildProperties.ToImmutableDictionary()
                )
            );
        }

        // To run generators, we can use an empty compilation.

        var compilation = CSharpCompilation.Create(
                GetType().Name,
                [
                    CSharpSyntaxTree.ParseText(source)
                ],
                options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
            )
            .WithReferences(ReferencesHelper.References)
            .AddSyntaxTrees(additionalSources.Select(x => CSharpSyntaxTree.ParseText(x)));

        foreach (var additionalPackage in runTestOptions.AdditionalPackages)
        {
            var downloaded = await NuGetDownloader.DownloadPackageAsync(additionalPackage.Id, additionalPackage.Version);

            compilation = compilation.AddReferences(downloaded);
        }

        // Run generators. Don't forget to use the new compilation rather than the previous one.
        driver.RunGeneratorsAndUpdateCompilation(compilation, out var newCompilation, out var diagnostics);

        foreach (var error in diagnostics.Where(IsError))
        {
            throw new Exception
            (
                $"""
                  There was an error with the compilation.
                  Have you added required references and additional files?

                  {error}

                  {string.Join(Environment.NewLine, newCompilation.SyntaxTrees.Select(x => x.GetText()))}
                 """
            );
        }

        // Retrieve all files in the compilation.
        var generatedFiles = newCompilation.SyntaxTrees
            .Select(t => t.GetText().ToString())
            .Except([source])
            .Except(additionalSources)
            .ToArray();

        foreach (var error in diagnostics.Where(x => x.Severity == DiagnosticSeverity.Error))
        {
            throw new Exception(
                $"There was an error with the generator compilation.{Environment.NewLine}{Environment.NewLine}{error}{Environment.NewLine}{Environment.NewLine}{string.Join(Environment.NewLine, generatedFiles)}");
        }

        await assertions(generatedFiles);

        // Scrub GUIDs from generated files before verification
        var scrubbedFiles = generatedFiles.Select(file => Scrub(file)).ToArray();
        var verifyTask = Verify(scrubbedFiles)
            .ScrubFilePaths();

        if (runTestOptions.VerifyConfigurator != null)
        {
            verifyTask = runTestOptions.VerifyConfigurator(verifyTask);
        }
        else
        {
            verifyTask = verifyTask.OnVerifyMismatch(async (pair, message, verify) =>
            {
                var received = await File.ReadAllTextAsync(pair.ReceivedPath);
                var verified = await File.ReadAllTextAsync(pair.VerifiedPath);

                // Better diff message since original one is too large
                await Assert.That(Scrub(received)).IsEqualTo(Scrub(verified));
            });
        }

        await verifyTask;
    }

    private static bool IsError(Diagnostic x)
    {
        if (x.Severity == DiagnosticSeverity.Error)
        {
            return true;
        }

        if (x.Severity == DiagnosticSeverity.Warning && x.GetMessage().Contains("failed to generate source"))
        {
            return true;
        }

        return false;
    }

    private StringBuilder Scrub(StringBuilder text)
    {
        var result = text
            .Replace("\r\n", "\n")
            .Replace("\r", "\n")
            .Replace("\\r\\n", "\\n")
            .Replace("\\r", "\\n");

        // Scrub GUIDs from class names and identifiers
        // Pattern 1: TestSource classes - ClassName_MethodName_TestSource_[32 hex chars]
        var guidPattern1 = @"_TestSource_[a-fA-F0-9]{32}";
        var scrubbedText = System.Text.RegularExpressions.Regex.Replace(result.ToString(), guidPattern1, "_TestSource_GUID", System.Text.RegularExpressions.RegexOptions.None);

        // Pattern 2: ModuleInitializer classes - ClassName_MethodName_ModuleInitializer_[32 hex chars]
        var guidPattern2 = @"_ModuleInitializer_[a-fA-F0-9]{32}";
        scrubbedText = System.Text.RegularExpressions.Regex.Replace(scrubbedText, guidPattern2, "_ModuleInitializer_GUID", System.Text.RegularExpressions.RegexOptions.None);

        var guidPattern3 = @"_(Before|After|BeforeEvery|AfterEvery)_(Test|Class|Assembly|TestSession|TestDiscovery)_[a-fA-F0-9]{32}";
        scrubbedText = System.Text.RegularExpressions.Regex.Replace(scrubbedText, guidPattern3, "_$1_$2_GUID", System.Text.RegularExpressions.RegexOptions.None);

        // Scrub file paths - Windows style (e.g., D:\\git\\TUnit\\)
        var windowsPathPattern = @"[A-Za-z]:\\\\[^""'\s,)]+";
        scrubbedText = System.Text.RegularExpressions.Regex.Replace(scrubbedText, windowsPathPattern, "PATH_SCRUBBED");

        // Scrub file paths - Unix style (e.g., /home/user/...)
        var unixPathPattern = @"/[a-zA-Z0-9_\-./]+/[a-zA-Z0-9_\-./]+";
        scrubbedText = System.Text.RegularExpressions.Regex.Replace(scrubbedText, unixPathPattern, "PATH_SCRUBBED");

        return new StringBuilder(scrubbedText);
    }

    private string Scrub(string text)
    {
        return Scrub(new StringBuilder(text)).ToString();
    }
}
